# Copyright 2019 Google, Inc.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met: redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer;
# redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution;
# neither the name of the copyright holders nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

from __future__ import print_function

Import('*')

from m5.util.grammar import Grammar

from gem5_scons import Transform, warning, error

import os.path

if env['USE_ARM_FASTMODEL']:
    if not env['USE_SYSTEMC']:
        warning('ARM Fast Models require systemc support')
        env['USE_ARM_FASTMODEL'] = False
        Return()

if not env['USE_ARM_FASTMODEL']:
    Return()


systemc_home = Dir('#/src/systemc/ext/systemc_home')
env['ENV']['SYSTEMC_HOME'] = systemc_home.abspath

def extract_var(name):
    if name not in env:
        error('Error: %s is not set' % name)
    print('%s = %s' % (name, env[name]))
    # Make sure the value of this variable shows up as an environment variable
    # for commands scons runs.
    env['ENV'][name] = env[name]
    return env[name]

pvlib_home, maxcore_home, armlmd_license_file = \
    list(map(extract_var, ('PVLIB_HOME', 'MAXCORE_HOME',
                           'ARMLMD_LICENSE_FILE')))

pvlib_home = Dir(pvlib_home)
maxcore_home = Dir(maxcore_home)
armlmd_license_file = File(armlmd_license_file)


pvlib_flavor = env['PVLIB_FLAVOR']
pvlib_lib_dir = pvlib_home.Dir('lib').Dir(pvlib_flavor)

simulation_engine_name = 'libMAXCOREInitSimulationEngine.3.so'
simulation_engine_lib = pvlib_lib_dir.File(simulation_engine_name)


def staticify(env, name):
    ''' Check for a static version of the library named 'name', and if it
        exists return a File node for it explicitly. Otherwise pass 'name'
        through for normal processing.'''

    static_name = env.subst('${LIBPREFIX}' + name + '${LIBSUFFIX}')

    for path in env.Flatten(env['LIBPATH']):
        full_name = Dir(path).File(static_name).get_abspath()
        if os.path.isfile(full_name):
            return File(full_name)

    return name


# Adjust the build environment to support building in Fast Models.

env.Append(CCFLAGS='-pthread')

cpppaths = (
    pvlib_home.Dir('include/fmruntime'),
    pvlib_home.Dir('include/fmruntime/eslapi'),
    pvlib_home.Dir('Iris/include'),

    systemc_home.Dir('include'),

    maxcore_home.Dir('AMBA-PV/include'),
)
env.Append(CPPPATH=cpppaths)

lib_paths = (
    pvlib_lib_dir,
    pvlib_home.Dir('Iris').Dir(pvlib_flavor),
)
env.Append(LIBPATH=lib_paths)
env.Append(RPATH=lib_paths)

libs = (
    'components',
    'pvbus',
    'armctmodel',
    'fmruntime',
    'IrisSupport',
    'dl',
    'rt',
)
env.Append(LIBS=list(staticify(env, lib) for lib in libs))


class ProjectFileParser(Grammar):
    class Param(object):
        def __init__(self, is_object):
            self.is_object = is_object

        def __str__(self):
            return self._to_str(0)

    class StringParam(Param):
        def __init__(self, name, value):
            super(ProjectFileParser.StringParam, self).__init__(
                    is_object=False)
            self.name = name
            self.value = value

        def _to_str(self, indent):
            indent_str = "    " * indent
            return indent_str + self.name + ' = \"' + self.value + '\"'

    class ObjectParam(Param):
        def __init__(self, type_name, name, params):
            super(ProjectFileParser.ObjectParam, self).__init__(
                    is_object=True)
            self.type_name = type_name
            self.name = name
            self.params = params

        def _to_str(self, indent):
            indent_str = "    " * indent
            val = indent_str + self.type_name
            if self.name is not None:
                val += ' "' + self.name + '"'
            val += "\n" + indent_str + "{\n"
            for param in self.params:
                val += param._to_str(indent + 1) + "\n"
            val += indent_str + "}"
            return val

    #########
    # Lexer #
    #########

    tokens = (
        # identifier
        'ID',

        # string literal
        'STRLIT',

        # = { } ;
        'EQUALS',
        'LBRACE',
        'RBRACE',
        'SEMI',
    )

    t_ID = r'[A-Za-z_]\w*'

    def t_STRLIT(self, t):
        r'(?m)"([^"])*"'
        # strip off quotes
        t.value = t.value[1:-1]
        t.lexer.lineno += t.value.count('\n')
        return t

    t_EQUALS = r'='
    t_LBRACE = r'\{'
    t_RBRACE = r'\}'
    t_SEMI = r';'

    def t_NEWLINE(self, t):
        r'\n+'
        t.lexer.lineno += t.value.count('\n')

    t_ignore = ' \t\x0c'

    ##########
    # Parser #
    ##########

    def p_object(self, t):
        'object : object_heading LBRACE params RBRACE'
        t[0] = self.ObjectParam(t[1][0], t[1][1], t[3])

    def p_object_heading_0(self, t):
        'object_heading : ID STRLIT'
        t[0] = (t[1], t[2])

    def p_object_heading_1(self, t):
        'object_heading : ID'
        t[0] = (t[1], None)

    def p_params_0(self, t):
        'params : '
        t[0] = []

    def p_params_1(self, t):
        'params : param params'
        t[0] = [t[1]] + t[2]

    def p_param_0(self, t):
        'param : ID EQUALS STRLIT SEMI'
        t[0] = self.StringParam(t[1], t[3])

    def p_param_1(self, t):
        'param : object'
        t[0] = t[1]


class ArmFastModelComponent(object):
    def __init__(self, project_file, *extra_deps):
        project_file = File(project_file)
        project_file_dir = project_file.Dir('.')

        parser = ProjectFileParser()
        proj = parser.parse_file(project_file.srcnode().abspath)

        # Top level component.
        tlc = None
        # Config name.
        config_name = None

        # Scan for particular properties of the project.
        for param in proj.params:
            if not param.is_object:
                if param.name == 'TOP_LEVEL_COMPONENT':
                    tlc = param.value
                elif param.name == 'ACTIVE_CONFIG_LINUX':
                    config_name = param.value

        assert tlc is not None and config_name is not None

        simgen_dir = project_file_dir.Dir(config_name)

        def simgen_static(name):
            return simgen_dir.File(env.subst(
                        '${LIBPREFIX}%s${LIBSUFFIX}' % name))

        def simgen_shared(name):
            return simgen_dir.File(env.subst(
                        '${SHLIBPREFIX}%s${SHLIBSUFFIX}' % name))

        static_libs = [
            'scx-%s-%s' % (tlc, config_name),
            'scx',
        ]
        shared_libs = [
            '%s-%s' % (tlc, config_name),
        ]

        static_lib_nodes = list(map(simgen_static, static_libs))
        shared_lib_nodes = list(map(simgen_shared, shared_libs))
        # We need to use the static libraries as files so that the linker
        # doesn't use the shared versions of them instead. We need to use
        # the shared libraries by name so that the linker will apply RPATH
        # and be able to find them at run time. If a shared libary includes
        # a path, the dynamic linker will apparently ignore RPATH when looking
        # for it.
        lib_nodes = static_lib_nodes + shared_lib_nodes

        gen_dir = simgen_dir.Dir('gen')

        header = gen_dir.File('scx_evs_%s.h' % tlc)

        self.headers = [header]
        self.headerpaths = [gen_dir]
        self.libs = static_lib_nodes + shared_libs
        self.libpaths = [simgen_dir]
        self.rpaths = [simgen_dir]

        simgen_cmd = env.subst('${SIMGEN} -p %s --configuration %s -b ' +
            '--verbose off --num-build-cpus 100 --build-dir %s') % \
            (project_file.srcnode().abspath, config_name, simgen_dir.abspath)
        if not GetOption('verbose'):
            simgen_cmd += ' > /dev/null'
        simgen_action = MakeAction(simgen_cmd, Transform('SIMGEN'))
        sources = [project_file]
        sources.extend(extra_deps)
        env.Command(lib_nodes + self.headers, sources, simgen_action)

    def prepare_env(self, env):
        env.Append(LIBPATH=self.libpaths)
        env.AddLocalRPATH(*self.rpaths)
        env.Append(CPPPATH=self.headerpaths)
        env.Prepend(LIBS=self.libs)


class ArmFastModelBin(Executable):
    def __init__(self, target, *components_and_sources):
        def is_component(x):
            return isinstance(x, ArmFastModelComponent)

        def not_component(x):
            return not is_component(x)

        components = list(filter(is_component, components_and_sources))
        sources = list(filter(not_component, components_and_sources))

        self.components = components
        super(ArmFastModelBin, self).__init__(target, *sources)

    @classmethod
    def declare_all(cls, env):
        env = env.Clone()
        env.Prepend(LIBS=env['STATIC_LIB'][0])
        super(ArmFastModelBin, cls).declare_all(env)

    def declare(self, env):
        env = env.Clone()

        sources = list(self.sources)
        for f in self.filters:
            sources += Source.all.apply_filter(f)

        objs = self.srcs_to_objs(env, sources)
        objs = objs + env['MAIN_OBJS']

        for component in self.components:
            component.prepare_env(env)

        binary = super(ArmFastModelBin, self).declare(env, objs)[0]

        # We need a copy of the simulation engine lib alongside the executable
        # so that the license check works properly.
        local_engine = binary.File(simulation_engine_name)
        Depends(binary, local_engine)
        main.Command(local_engine, simulation_engine_lib,
                MakeAction("cp ${SOURCE} ${TARGET}", Transform('COPY')))

Export('ArmFastModelComponent')
Export('ArmFastModelBin')

PySource('m5', 'arm_fast_model.py')
Source('fastmodel.cc')

SimObject('FastModel.py')
Source('amba_to_tlm_bridge.cc')
Source('amba_from_tlm_bridge.cc')

# HACK: Make sure the gic protocol headers are somewhere we can find them.
# These should start out alongside other headers fast model provides which we
# are already able to include, but unfortunately they're in the examples
# directory.
gicv3_comms_headers = (
        'gicv3_comms_base.h', 'gicv3_comms_if.h', 'gicv3_comms_sockets.h')
examples_common_dir = pvlib_home.Dir('examples/SystemCExport/Common')
gic_protocol_path = 'Protocols/GICv3Comms'
gic_protocol_dest = Dir(env['BUILDDIR']).Dir(gic_protocol_path)
gic_protocol_src = examples_common_dir.Dir(gic_protocol_path)

for header in gicv3_comms_headers:
    Command(gic_protocol_dest.File(header), gic_protocol_src.File(header),
            Copy('${TARGET}', '${SOURCE}'))

lisa_protocol_types_header_path = 'include/lisa_protocol_types.h'
lisa_protocol_types_header_target = \
    gic_protocol_dest.File(lisa_protocol_types_header_path)
lisa_protocol_types_header_src = \
    examples_common_dir.File(lisa_protocol_types_header_path)
Command(lisa_protocol_types_header_target, lisa_protocol_types_header_src,
        Copy('${TARGET}', '${SOURCE}'))
